From f18fb169c5b5fe00e8d50683a8595d19e3521235 Mon Sep 17 00:00:00 2001 From: brandon-schabel Date: Fri, 3 Nov 2023 00:34:23 -0700 Subject: [PATCH 1/5] improved sqlite table generation config and improved unit testing --- modules/sqlite/README.md | 6 +- modules/sqlite/index.ts | 2 +- modules/sqlite/sqlite-factory.test.ts | 4 +- modules/sqlite/sqlite-factory.ts | 60 +-- modules/sqlite/sqlite-table-factory.test.ts | 6 +- modules/sqlite/sqlite-table-factory.ts | 12 +- modules/sqlite/sqlite-types.ts | 0 .../sqlite/sqlite-utils/crud-fn-utils.test.ts | 6 +- modules/sqlite/sqlite-utils/crud-fn-utils.ts | 8 +- .../sqlite-utils/format-foreign-keys.test.ts | 14 - .../sqlite-utils/format-foreign-keys.ts | 13 - .../sqlite/sqlite-utils/format-schema.test.ts | 8 +- modules/sqlite/sqlite-utils/format-schema.ts | 13 +- .../sqlite-utils/table-query-gen.test.ts | 342 ++++++++++++++++++ .../sqlite/sqlite-utils/table-query-gen.ts | 85 +++++ .../sqlite-utils/table-query-string.test.ts | 41 --- .../sqlite/sqlite-utils/table-query-string.ts | 38 -- 17 files changed, 494 insertions(+), 164 deletions(-) create mode 100644 modules/sqlite/sqlite-types.ts delete mode 100644 modules/sqlite/sqlite-utils/format-foreign-keys.test.ts delete mode 100644 modules/sqlite/sqlite-utils/format-foreign-keys.ts create mode 100644 modules/sqlite/sqlite-utils/table-query-gen.test.ts create mode 100644 modules/sqlite/sqlite-utils/table-query-gen.ts delete mode 100644 modules/sqlite/sqlite-utils/table-query-string.test.ts delete mode 100644 modules/sqlite/sqlite-utils/table-query-string.ts diff --git a/modules/sqlite/README.md b/modules/sqlite/README.md index 0361924..cd557c9 100644 --- a/modules/sqlite/README.md +++ b/modules/sqlite/README.md @@ -20,9 +20,9 @@ Next, define your schema and use `sqliteTableFactory` to generate your table. ```javascript const userSchema = { - id: "TEXT", - name: "TEXT", - email: "TEXT", + id: { type: "TEXT" }, + name: { type: "TEXT" }, + email: { type: "TEXT" }, } const db = new Database({filename: "./mydb.sqlite"}) diff --git a/modules/sqlite/index.ts b/modules/sqlite/index.ts index 549fdef..dd90782 100644 --- a/modules/sqlite/index.ts +++ b/modules/sqlite/index.ts @@ -5,5 +5,5 @@ export { readItems, updateItem } from "./sqlite-utils/crud-fn-utils"; -export { createTableQuery } from "./sqlite-utils/table-query-string"; +export { createTableQuery } from "./sqlite-utils/table-query-gen"; diff --git a/modules/sqlite/sqlite-factory.test.ts b/modules/sqlite/sqlite-factory.test.ts index dcbe329..35d0be0 100644 --- a/modules/sqlite/sqlite-factory.test.ts +++ b/modules/sqlite/sqlite-factory.test.ts @@ -5,8 +5,8 @@ import { SchemaMap, createSqliteFactory } from "./sqlite-factory"; let db = new Database(":memory:"); const noteSchema = { - id: "TEXT", - text: "TEXT", + id: { type: "TEXT" }, + text: { type: "TEXT" }, } satisfies SchemaMap; describe("createSqliteFactory", () => { diff --git a/modules/sqlite/sqlite-factory.ts b/modules/sqlite/sqlite-factory.ts index 886750b..7a09048 100644 --- a/modules/sqlite/sqlite-factory.ts +++ b/modules/sqlite/sqlite-factory.ts @@ -6,11 +6,11 @@ import { } from "./sqlite-table-factory"; export type CreateSqliteFactory = { - create: (item: SQLiteSchemaToTypeScript) => Promise; - read: () => Promise[]>; + create: (item: SQLiteSchemaInfer) => Promise; + read: () => Promise[]>; update: ( id: number, - item: Partial> + item: Partial> ) => Promise; deleteById: (id: number) => Promise; }; @@ -38,44 +38,52 @@ export type SQLiteToTypeScriptTypes = { DATE: Date; }; -export type SchemaKeys = keyof SQLiteToTypeScriptTypes; +export type SQLiteDataTypes = keyof SQLiteToTypeScriptTypes; -export type SchemaMap = Partial>; - -export const createTableSchema = ( - schema: Schema -): string => { - return undefined as any as string; +export type FieldDefinition = { + type: SQLiteDataTypes; // The data type of the field + primaryKey?: boolean; // Whether the field is a primary key + unique?: boolean; // Whether the field should be unique + foreignKey?: string; // Reference to another table (foreign key) + notNull?: boolean; // Whether the field can be null + defaultValue?: string | number; // Default value for the field }; // Mapped type that takes a schema with SQLite types and returns a schema with TypeScript types. -export type SQLiteSchemaToTypeScript = { - [K in keyof T]: T[K] extends SchemaKeys - ? SQLiteToTypeScriptTypes[T[K]] +export type SQLiteSchemaInfer = { + [K in keyof T]: T[K] extends FieldDefinition + ? SQLiteToTypeScriptTypes[T[K]["type"]] : never; }; -// Example usage. +export type SchemaMap = Partial>; + +// example const sqlitePersonTableSchema = { - id: "TEXT", - age: "INTEGER", - name: "TEXT", - createdAt: "DATE", + id: { type: "TEXT" }, + age: { type: "INTEGER" }, + name: { type: "TEXT" }, + createdAt: { type: "DATE" }, } satisfies SchemaMap; +type Person = SQLiteSchemaInfer; + +// TODO implement into the sqlite factory +type PersonTableSchema = SQLiteSchemaInfer; + +export const createTableSchema = ( + schema: Schema +): string => { + return undefined as any as string; +}; + export const getType = ( schema: T -): SQLiteSchemaToTypeScript => { - return undefined as any as SQLiteSchemaToTypeScript; +): SQLiteSchemaInfer => { + return undefined as any as SQLiteSchemaInfer; }; -type Person = SQLiteSchemaToTypeScript; -// => schema; -// TODO implement into the sqlite factory -type PersonTableSchema = SQLiteSchemaToTypeScript< - typeof sqlitePersonTableSchema ->; // This should now have the correct types. let person: PersonTableSchema = { diff --git a/modules/sqlite/sqlite-table-factory.test.ts b/modules/sqlite/sqlite-table-factory.test.ts index 2cb1ee8..7962ca3 100644 --- a/modules/sqlite/sqlite-table-factory.test.ts +++ b/modules/sqlite/sqlite-table-factory.test.ts @@ -5,9 +5,9 @@ import { sqliteTableFactory } from "./sqlite-table-factory"; const mockDb = new Database(":memory:"); const testSchema = { - id: "TEXT", - name: "TEXT", - age: "INTEGER", + id: { type: "TEXT" }, + name: { type: "TEXT" }, + age: { type: "INTEGER" }, } satisfies SchemaMap; const factoryOptions = { diff --git a/modules/sqlite/sqlite-table-factory.ts b/modules/sqlite/sqlite-table-factory.ts index 271b8d7..3a268e4 100644 --- a/modules/sqlite/sqlite-table-factory.ts +++ b/modules/sqlite/sqlite-table-factory.ts @@ -1,7 +1,7 @@ import Database from "bun:sqlite"; import { CreateSqliteFactory, - SQLiteSchemaToTypeScript, + SQLiteSchemaInfer, SchemaMap, } from "./sqlite-factory"; import { @@ -10,7 +10,7 @@ import { readItems, updateItem, } from "./sqlite-utils/crud-fn-utils"; -import { createTableQuery } from "./sqlite-utils/table-query-string"; +import { createTableQuery } from "./sqlite-utils/table-query-gen"; export type ForeignKeysT = | { column: keyof Schema; references: string }[] @@ -39,17 +39,17 @@ function logger(debug: boolean) { export function sqliteTableFactory< Schema extends SchemaMap, - TranslatedSchema extends SQLiteSchemaToTypeScript = SQLiteSchemaToTypeScript + TranslatedSchema extends SQLiteSchemaInfer = SQLiteSchemaInfer >( params: SqliteTableFactoryParams, options: SqliteTableOptions = {} ) { const { db, schema, tableName } = params; - const { debug = false, foreignKeys = null } = options; + const { debug = false } = options; const log = logger(debug); - db.query(createTableQuery({ tableName, schema, foreignKeys, debug })).run(); + db.query(createTableQuery({ tableName, schema, debug })).run(); // Pass necessary context to external CRUD functions function create(item: TranslatedSchema) { @@ -67,7 +67,7 @@ export function sqliteTableFactory< updateItem(db, tableName, log, id, item); } - function deleteById(id: number| string) { + function deleteById(id: number | string) { deleteItemById(db, tableName, log, id); } diff --git a/modules/sqlite/sqlite-types.ts b/modules/sqlite/sqlite-types.ts new file mode 100644 index 0000000..e69de29 diff --git a/modules/sqlite/sqlite-utils/crud-fn-utils.test.ts b/modules/sqlite/sqlite-utils/crud-fn-utils.test.ts index 35f973c..1a4c69d 100644 --- a/modules/sqlite/sqlite-utils/crud-fn-utils.test.ts +++ b/modules/sqlite/sqlite-utils/crud-fn-utils.test.ts @@ -9,9 +9,9 @@ import { } from "./crud-fn-utils"; // replace with the path to your file const testSchema = { - id: "TEXT", - name: "TEXT", - age: "INTEGER", + id: { type: "TEXT" }, + name: { type: "TEXT" }, + age: { type: "INTEGER" }, } satisfies SchemaMap; let db = new Database(":memory:"); diff --git a/modules/sqlite/sqlite-utils/crud-fn-utils.ts b/modules/sqlite/sqlite-utils/crud-fn-utils.ts index 97edc8a..f9989a5 100644 --- a/modules/sqlite/sqlite-utils/crud-fn-utils.ts +++ b/modules/sqlite/sqlite-utils/crud-fn-utils.ts @@ -1,5 +1,5 @@ import Database from "bun:sqlite"; -import { SchemaMap, SQLiteSchemaToTypeScript } from "../sqlite-factory"; +import { SchemaMap, SQLiteSchemaInfer } from "../sqlite-factory"; import { deleteQueryString, insertQueryString, @@ -9,7 +9,7 @@ import { export function createItem< Schema extends SchemaMap, - TranslatedSchema extends SQLiteSchemaToTypeScript = SQLiteSchemaToTypeScript + TranslatedSchema extends SQLiteSchemaInfer = SQLiteSchemaInfer >( db: Database, tableName: string, @@ -25,7 +25,7 @@ export function createItem< export function readItems< Schema extends SchemaMap, - TranslatedSchema extends SQLiteSchemaToTypeScript = SQLiteSchemaToTypeScript + TranslatedSchema extends SQLiteSchemaInfer = SQLiteSchemaInfer >( db: Database, tableName: string, @@ -39,7 +39,7 @@ export function readItems< export function updateItem< Schema extends SchemaMap, - TranslatedSchema extends SQLiteSchemaToTypeScript = SQLiteSchemaToTypeScript + TranslatedSchema extends SQLiteSchemaInfer = SQLiteSchemaInfer >( db: Database, tableName: string, diff --git a/modules/sqlite/sqlite-utils/format-foreign-keys.test.ts b/modules/sqlite/sqlite-utils/format-foreign-keys.test.ts deleted file mode 100644 index 8bfbec5..0000000 --- a/modules/sqlite/sqlite-utils/format-foreign-keys.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { expect, test } from "bun:test"; -import { formatForeignKeys } from "./format-foreign-keys"; - -test("formatForeignKeys formats foreign keys correctly", () => { - const foreignKeys = [ - { - column: "id", - references: "other_table(id)", - }, - ]; - - const result = formatForeignKeys(foreignKeys); - expect(result).toBe("FOREIGN KEY (id) REFERENCES other_table(id)"); -}); diff --git a/modules/sqlite/sqlite-utils/format-foreign-keys.ts b/modules/sqlite/sqlite-utils/format-foreign-keys.ts deleted file mode 100644 index d7ca142..0000000 --- a/modules/sqlite/sqlite-utils/format-foreign-keys.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { SQLiteSchemaToTypeScript } from "../sqlite-factory"; -import { ForeignKeysT } from "../sqlite-table-factory"; - -export function formatForeignKeys>( - foreignKeys: ForeignKeysT | undefined -): string { - if (!foreignKeys) return ""; - return foreignKeys - .map( - (fk) => `FOREIGN KEY (${String(fk.column)}) REFERENCES ${fk.references}` - ) - .join(", "); -} diff --git a/modules/sqlite/sqlite-utils/format-schema.test.ts b/modules/sqlite/sqlite-utils/format-schema.test.ts index 3716934..a3450ca 100644 --- a/modules/sqlite/sqlite-utils/format-schema.test.ts +++ b/modules/sqlite/sqlite-utils/format-schema.test.ts @@ -4,11 +4,13 @@ import { formatSchema } from "./format-schema"; test("formatSchema formats schema correctly", () => { const schema = { - id: "INTEGER", - name: "TEXT", + id: { type: "INTEGER" }, + name: { type: "TEXT" }, } satisfies SchemaMap; // TODO need to fix schema type const result = formatSchema(schema); - expect(result).toBe("id INTEGER, name TEXT"); + + expect(result[0]).toBe("id INTEGER"); + expect(result[1]).toBe("name TEXT"); }); diff --git a/modules/sqlite/sqlite-utils/format-schema.ts b/modules/sqlite/sqlite-utils/format-schema.ts index bb634f8..33cb6a2 100644 --- a/modules/sqlite/sqlite-utils/format-schema.ts +++ b/modules/sqlite/sqlite-utils/format-schema.ts @@ -1,10 +1,9 @@ import { SchemaMap } from "../sqlite-factory"; export function formatSchema( - schema: Schema - ): string { - return Object.entries(schema) - .map(([key, type]) => `${key} ${type?.toUpperCase()}`) - .join(", "); - } - \ No newline at end of file + schema: Schema +): string[] { + return Object.entries(schema).map( + ([key, fieldDefinition]) => `${key} ${fieldDefinition?.type.toUpperCase()}` + ); +} diff --git a/modules/sqlite/sqlite-utils/table-query-gen.test.ts b/modules/sqlite/sqlite-utils/table-query-gen.test.ts new file mode 100644 index 0000000..7b1e401 --- /dev/null +++ b/modules/sqlite/sqlite-utils/table-query-gen.test.ts @@ -0,0 +1,342 @@ +import { describe, expect, it, test } from "bun:test"; +import { FieldDefinition, SchemaMap } from "../sqlite-factory"; +import { + assembleCreateTableQuery, + createColumnDefinition, + createTableLevelConstraint, + createTableQuery, +} from "./table-query-gen"; + +test("createTableQuery constructs SQL query correctly with foreign keys", () => { + const schema = { + id: { type: "INTEGER" }, + name: { type: "TEXT" }, + fkTest: { type: "TEXT", foreignKey: "other_table(id)" }, + } satisfies SchemaMap; + + const result = createTableQuery({ + schema, + tableName: "test_table", + }); + + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `name` TEXT, `fkTest` TEXT, FOREIGN KEY (`fkTest`) REFERENCES other_table(id));" + ); +}); + +test("createTableQuery constructs SQL query correctly without foreign keys", () => { + const schema = { + id: { type: "INTEGER" }, + name: { type: "TEXT" }, + } satisfies SchemaMap; + + const result = createTableQuery({ + schema, + tableName: "test_table", + }); + + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `name` TEXT);" + ); +}); + +describe("createColumnDefinition", () => { + it("should generate a column definition for a simple TEXT field", () => { + const definition: FieldDefinition = { type: "TEXT" }; + const result = createColumnDefinition("name", definition); + expect(result).toBe("`name` TEXT"); + }); + + it("should generate a column definition for an INTEGER field with a PRIMARY KEY", () => { + const definition: FieldDefinition = { type: "INTEGER", primaryKey: true }; + const result = createColumnDefinition("id", definition); + expect(result).toBe("`id` INTEGER PRIMARY KEY"); + }); + + // Add tests for unique, notNull, and defaultValue... + + it("should throw an error if the definition is not provided", () => { + expect(() => { + createColumnDefinition("age", undefined as unknown as FieldDefinition); + }).toThrow(); + }); + + it("should generate a column definition with a UNIQUE constraint", () => { + const definition: FieldDefinition = { type: "TEXT", unique: true }; + const result = createColumnDefinition("username", definition); + expect(result).toBe("`username` TEXT UNIQUE"); + }); + + it("should generate a column definition with a NOT NULL constraint", () => { + const definition: FieldDefinition = { type: "INTEGER", notNull: true }; + const result = createColumnDefinition("age", definition); + expect(result).toBe("`age` INTEGER NOT NULL"); + }); + + it("should generate a column definition with a DEFAULT value", () => { + const definition: FieldDefinition = { type: "TEXT", defaultValue: "N/A" }; + const result = createColumnDefinition("status", definition); + expect(result).toBe("`status` TEXT DEFAULT N/A"); + }); + + it("should correctly quote a DEFAULT string value", () => { + const definition: FieldDefinition = { + type: "TEXT", + defaultValue: "'active'", + }; + const result = createColumnDefinition("state", definition); + expect(result).toBe("`state` TEXT DEFAULT 'active'"); + }); + + it("should generate a column definition with multiple constraints", () => { + const definition: FieldDefinition = { + type: "INTEGER", + notNull: true, + unique: true, + defaultValue: 0, + }; + const result = createColumnDefinition("count", definition); + expect(result).toContain("`count` INTEGER"); + expect(result).toContain("NOT NULL"); + expect(result).toContain("DEFAULT 0"); + }); + + it("should not include DEFAULT when defaultValue is not provided", () => { + const definition: FieldDefinition = { type: "REAL" }; + const result = createColumnDefinition("price", definition); + expect(result).toBe("`price` REAL"); + }); + + it("should handle numeric DEFAULT values correctly", () => { + const definition: FieldDefinition = { type: "INTEGER", defaultValue: 10 }; + const result = createColumnDefinition("quantity", definition); + expect(result).toBe("`quantity` INTEGER DEFAULT 10"); + }); + + // Test for an edge case where type is unknown + // it("should throw an error if an invalid type is provided", () => { + // const definition = { type: "INVALID_TYPE" } as unknown as FieldDefinition; + // expect(() => { + // createColumnDefinition("invalid", definition); + // }).toThrow(); + // }); + + // Test for proper escaping of field names that are SQL keywords + it("should escape field names that are SQL keywords", () => { + const definition: FieldDefinition = { type: "TEXT" }; + const result = createColumnDefinition("group", definition); + expect(result).toBe("`group` TEXT"); + }); +}); + +describe("createTableLevelConstraint", () => { + it("should generate a foreign key constraint", () => { + const definition: FieldDefinition = { + type: "INTEGER", + foreignKey: "other_table(id)", + }; + const result = createTableLevelConstraint("fkTest", definition); + expect(result).toBe("FOREIGN KEY (`fkTest`) REFERENCES other_table(id)"); + }); + + it("should return null if no foreign key is defined", () => { + const definition: FieldDefinition = { type: "INTEGER" }; + const result = createTableLevelConstraint("fkTest", definition); + expect(result).toBeNull(); + }); + + it("should generate a foreign key constraint with a custom reference field", () => { + const definition: FieldDefinition = { + type: "INTEGER", + foreignKey: "other_table(custom_id)", + }; + const result = createTableLevelConstraint("fkCustomId", definition); + expect(result).toBe( + "FOREIGN KEY (`fkCustomId`) REFERENCES other_table(custom_id)" + ); + }); + + it("should properly trim the foreign key definition", () => { + const definition: FieldDefinition = { + type: "INTEGER", + foreignKey: " other_table (custom_id) ", + }; + const result = createTableLevelConstraint("fkCustomId", definition); + expect(result).toBe( + "FOREIGN KEY (`fkCustomId`) REFERENCES other_table(custom_id)" + ); + }); + + it("should handle foreign keys that include spaces or special characters", () => { + const definition: FieldDefinition = { + type: "TEXT", + foreignKey: "`other table`(`special id`)", + }; + const result = createTableLevelConstraint("fkSpecial", definition); + expect(result).toBe( + "FOREIGN KEY (`fkSpecial`) REFERENCES `other table`(`special id`)" + ); + }); + + it("should correctly format multiple foreign keys for a single field", () => { + const definition: FieldDefinition = { + type: "INTEGER", + foreignKey: "table1(id), table2(id)", + }; + const result = createTableLevelConstraint("multiFk", definition); + // Assuming your database schema allows multiple foreign keys per field, + // which is not standard in SQLite or most RDBMS. + expect(result).toBe( + "FOREIGN KEY (`multiFk`) REFERENCES table1(id), FOREIGN KEY (`multiFk`) REFERENCES table2(id)" + ); + }); + + it("should return null for a malformed foreign key definition", () => { + const definition: FieldDefinition = { + type: "INTEGER", + foreignKey: "malformed", + }; + const result = createTableLevelConstraint("fkMalformed", definition); + expect(result).toBeNull(); + }); + + // Test for a case where the foreign key reference does not include a field + it("should return null if foreign key reference is incomplete", () => { + const definition: FieldDefinition = { + type: "INTEGER", + foreignKey: "other_table", + }; + const result = createTableLevelConstraint("fkIncomplete", definition); + expect(result).toBeNull(); + }); + + // Test for proper escaping of table and column names in foreign key definitions + it("should escape table and column names in foreign key definitions", () => { + const definition: FieldDefinition = { + type: "INTEGER", + foreignKey: "`other-table`(`id`)", + }; + const result = createTableLevelConstraint("fkEscaped", definition); + expect(result).toBe( + "FOREIGN KEY (`fkEscaped`) REFERENCES `other-table`(`id`)" + ); + }); +}); + +describe("assembleCreateTableQuery", () => { + it("should assemble a create table query with a single column", () => { + const columns = ["`id` INTEGER PRIMARY KEY"]; + const result = assembleCreateTableQuery("test_table", columns, []); + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER PRIMARY KEY);" + ); + }); + + it("should assemble a create table query with foreign key constraints", () => { + const columns = ["`id` INTEGER", "`name` TEXT"]; + const constraints = ["FOREIGN KEY (`id`) REFERENCES other_table(id)"]; + const result = assembleCreateTableQuery("test_table", columns, constraints); + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `name` TEXT, FOREIGN KEY (`id`) REFERENCES other_table(id));" + ); + }); + + it("should handle tables with multiple foreign key constraints", () => { + const columns = [ + "`id` INTEGER", + "`parent_id` INTEGER", + "`owner_id` INTEGER", + ]; + const constraints = [ + "FOREIGN KEY (`parent_id`) REFERENCES parents(id)", + "FOREIGN KEY (`owner_id`) REFERENCES owners(id)", + ]; + const result = assembleCreateTableQuery("test_table", columns, constraints); + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `test_table` (`id` INTEGER, `parent_id` INTEGER, `owner_id` INTEGER, FOREIGN KEY (`parent_id`) REFERENCES parents(id), FOREIGN KEY (`owner_id`) REFERENCES owners(id));" + ); + }); + + it("should include unique constraints at the table level", () => { + const columns = ["`id` INTEGER", "`email` TEXT"]; + const constraints = ["UNIQUE (`email`)"]; + const result = assembleCreateTableQuery("users", columns, constraints); + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `users` (`id` INTEGER, `email` TEXT, UNIQUE (`email`));" + ); + }); + + it("should return a query without any constraints if none are provided", () => { + const columns = ["`id` INTEGER", "`name` TEXT"]; + const result = assembleCreateTableQuery("simple_table", columns, []); + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `simple_table` (`id` INTEGER, `name` TEXT);" + ); + }); + + it("should escape table names that are SQL keywords", () => { + const columns = ["`id` INTEGER"]; + const result = assembleCreateTableQuery("group", columns, []); + expect(result).toBe("CREATE TABLE IF NOT EXISTS `group` (`id` INTEGER);"); + }); + + it("should throw an error if columns are empty", () => { + expect(() => { + assembleCreateTableQuery("empty_table", [], []); + }).toThrow(); + }); + + it("should properly format a table with default values", () => { + const columns = ["`id` INTEGER", "`name` TEXT DEFAULT 'Unknown'"]; + const result = assembleCreateTableQuery( + "default_values_table", + columns, + [] + ); + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `default_values_table` (`id` INTEGER, `name` TEXT DEFAULT 'Unknown');" + ); + }); + + it("should assemble a create table query with check constraints", () => { + const columns = ["`age` INTEGER"]; + const constraints = ["CHECK (`age` >= 18)"]; + const result = assembleCreateTableQuery( + "check_constraints_table", + columns, + constraints + ); + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `check_constraints_table` (`age` INTEGER, CHECK (`age` >= 18));" + ); + }); + + it("should handle a table with composite primary keys", () => { + const columns = ["`id` INTEGER", "`revision` INTEGER"]; + const constraints = ["PRIMARY KEY (`id`, `revision`)"]; + const result = assembleCreateTableQuery( + "composite_keys_table", + columns, + constraints + ); + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `composite_keys_table` (`id` INTEGER, `revision` INTEGER, PRIMARY KEY (`id`, `revision`));" + ); + }); + + // Test to ensure that backticks are used consistently + it("should use backticks for all identifiers", () => { + const columns = ["`id` INTEGER", "`name` TEXT"]; + const constraints = ["FOREIGN KEY (`id`) REFERENCES `other_table`(`id`)"]; + const result = assembleCreateTableQuery( + "backtick_test", + columns, + constraints + ); + expect(result).toBe( + "CREATE TABLE IF NOT EXISTS `backtick_test` (`id` INTEGER, `name` TEXT, FOREIGN KEY (`id`) REFERENCES `other_table`(`id`));" + ); + }); + + // Add more tests for complex tables, edge cases, etc... +}); diff --git a/modules/sqlite/sqlite-utils/table-query-gen.ts b/modules/sqlite/sqlite-utils/table-query-gen.ts new file mode 100644 index 0000000..7803821 --- /dev/null +++ b/modules/sqlite/sqlite-utils/table-query-gen.ts @@ -0,0 +1,85 @@ +import { FieldDefinition, SchemaMap } from "../sqlite-factory"; + +export function assembleCreateTableQuery( + tableName: string, + columns: string[], + tableLevelConstraints: string[] +): string { + const tableDefinition = [...columns, ...tableLevelConstraints] + .filter(Boolean) + .join(", "); + return `CREATE TABLE IF NOT EXISTS \`${tableName}\` (${tableDefinition});`; +} + +export function createTableLevelConstraint( + fieldName: string, + definition: FieldDefinition +): string | null { + if (definition.foreignKey) { + const [referencedTable, referencedField] = definition.foreignKey.split("("); + return `FOREIGN KEY (\`${fieldName}\`) REFERENCES ${referencedTable}(${referencedField}`; + } + return null; +} + +export function createColumnDefinition( + fieldName: string, + definition: FieldDefinition +): string { + if (!definition) throw new Error(`No definition for field ${fieldName}`); + const type = definition.type; + const constraints = []; + + if (definition.primaryKey) constraints.push("PRIMARY KEY"); + if (definition.unique) constraints.push("UNIQUE"); + if (definition.notNull) constraints.push("NOT NULL"); + if (definition.defaultValue !== undefined) { + constraints.push(`DEFAULT ${definition.defaultValue}`); + } + + return `\`${fieldName}\` ${type} ${constraints.join(" ")}`.trim(); +} + +export function createTableQuery({ + schema, + tableName, + debug = false, +}: { + tableName: string; + schema: Schema; + debug?: boolean; +}): string { + if (debug) { + console.info({ schema, tableName }); + } + + if (!schema) throw new Error(`No schema provided for table ${tableName}`); + + const columns = Object.keys(schema).map((fieldName) => { + return createColumnDefinition( + fieldName, + schema[fieldName] as FieldDefinition + ); + }); + const tableLevelConstraints = Object.keys(schema) + .map( + (fieldName) => + createTableLevelConstraint( + fieldName, + schema[fieldName] as FieldDefinition + ) || "" + ) + .filter(Boolean); + + const query = assembleCreateTableQuery( + tableName, + columns, + tableLevelConstraints + ); + + if (debug) { + console.info({ query, schema, tableName }); + } + + return query; +} diff --git a/modules/sqlite/sqlite-utils/table-query-string.test.ts b/modules/sqlite/sqlite-utils/table-query-string.test.ts deleted file mode 100644 index d7c0fd3..0000000 --- a/modules/sqlite/sqlite-utils/table-query-string.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { expect, test } from "bun:test"; -import { SchemaMap } from "../sqlite-factory"; -import { createTableQuery } from "./table-query-string"; - -test("createTableQuery constructs SQL query correctly with foreign keys", () => { - const schema = { - id: "INTEGER", - name: "TEXT", - } satisfies SchemaMap; - - const result = createTableQuery({ - schema, - tableName: "test_table", - foreignKeys: [ - { - column: "id", - references: "other_table(id)", - }, - ], - }); - - expect(result).toBe( - "CREATE TABLE IF NOT EXISTS test_table (id INTEGER, name TEXT, FOREIGN KEY (id) REFERENCES other_table(id));" - ); -}); - -test("createTableQuery constructs SQL query correctly without foreign keys", () => { - const schema = { - id: "INTEGER", - name: "TEXT", - } satisfies SchemaMap; - - const result = createTableQuery({ - schema, - tableName: "test_table", - }); - - expect(result).toBe( - "CREATE TABLE IF NOT EXISTS test_table (id INTEGER, name TEXT);" - ); -}); diff --git a/modules/sqlite/sqlite-utils/table-query-string.ts b/modules/sqlite/sqlite-utils/table-query-string.ts deleted file mode 100644 index 7c8c207..0000000 --- a/modules/sqlite/sqlite-utils/table-query-string.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { SQLiteSchemaToTypeScript, SchemaMap } from "../sqlite-factory"; -import { ForeignKeysT } from "../sqlite-table-factory"; -import { formatForeignKeys } from "./format-foreign-keys"; -import { formatSchema } from "./format-schema"; - -export function createTableQuery< - Schema extends SchemaMap, - TranslatedSchema extends SQLiteSchemaToTypeScript = SQLiteSchemaToTypeScript ->({ - schema, - tableName, - debug = false, - foreignKeys, -}: { - tableName: string; - schema: Schema; - debug?: boolean; - foreignKeys?: ForeignKeysT; -}): string { - if (debug) { - console.info({ schema, tableName }); - } - - const fields = formatSchema(schema); - const foreignKeysConstraints = formatForeignKeys(foreignKeys); - - let query = `CREATE TABLE IF NOT EXISTS ${tableName} (${fields});`; - - if (foreignKeysConstraints) { - query = `CREATE TABLE IF NOT EXISTS ${tableName} (${fields}, ${foreignKeysConstraints});`; - } - - if (debug) { - console.info({ query, fields, schema, tableName }); - } - - return query; -} From 20ee5ec638e803741f803452a0b11ff71ec75366 Mon Sep 17 00:00:00 2001 From: brandon-schabel Date: Fri, 3 Nov 2023 15:50:59 -0700 Subject: [PATCH 2/5] update table gen unit tests and fixes --- jwt-tokens.json | 1 - .../sqlite-utils/table-query-gen.test.ts | 23 +++++-------------- .../sqlite/sqlite-utils/table-query-gen.ts | 23 +++++++++++++++++-- 3 files changed, 27 insertions(+), 20 deletions(-) delete mode 100644 jwt-tokens.json diff --git a/jwt-tokens.json b/jwt-tokens.json deleted file mode 100644 index 81e82a5..0000000 --- a/jwt-tokens.json +++ /dev/null @@ -1 +0,0 @@ -{"invalidTokens":["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMzQ1LCJyb2xlcyI6WyJ1c2VyIl0sImV4cCI6MTY5ODk5NTQ4Nn0.Y1BLeUJoNVJPVFphd1RCMGlwc3JZZ2ZSK3NlUTl4OExrNnhzTjFGTW9Paz0","eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMzQ1LCJyb2xlcyI6WyJ1c2VyIl0sImV4cCI6MTY5ODk5NTQ4Nn0.Y1BLeUJoNVJPVFphd1RCMGlwc3JZZ2ZSK3NlUTl4OExrNnhzTjFGTW9Paz0","8daa9f9fcb783d1323fdab02d78da927b6eb81852ea56cba7ae2ea9e3c76c73121ba5facd92bc535"],"refreshTokens":[{"token":"8daa9f9fcb783d1323fdab02d78da927b6eb81852ea56cba7ae2ea9e3c76c73121ba5facd92bc535","exp":1699596686}]} \ No newline at end of file diff --git a/modules/sqlite/sqlite-utils/table-query-gen.test.ts b/modules/sqlite/sqlite-utils/table-query-gen.test.ts index 7b1e401..b5a1587 100644 --- a/modules/sqlite/sqlite-utils/table-query-gen.test.ts +++ b/modules/sqlite/sqlite-utils/table-query-gen.test.ts @@ -178,26 +178,14 @@ describe("createTableLevelConstraint", () => { ); }); - it("should correctly format multiple foreign keys for a single field", () => { - const definition: FieldDefinition = { - type: "INTEGER", - foreignKey: "table1(id), table2(id)", - }; - const result = createTableLevelConstraint("multiFk", definition); - // Assuming your database schema allows multiple foreign keys per field, - // which is not standard in SQLite or most RDBMS. - expect(result).toBe( - "FOREIGN KEY (`multiFk`) REFERENCES table1(id), FOREIGN KEY (`multiFk`) REFERENCES table2(id)" - ); - }); - it("should return null for a malformed foreign key definition", () => { const definition: FieldDefinition = { type: "INTEGER", foreignKey: "malformed", }; - const result = createTableLevelConstraint("fkMalformed", definition); - expect(result).toBeNull(); + expect(() => + createTableLevelConstraint("fkMalformed", definition) + ).toThrow(); }); // Test for a case where the foreign key reference does not include a field @@ -206,8 +194,9 @@ describe("createTableLevelConstraint", () => { type: "INTEGER", foreignKey: "other_table", }; - const result = createTableLevelConstraint("fkIncomplete", definition); - expect(result).toBeNull(); + expect(() => + createTableLevelConstraint("fkIncomplete", definition) + ).toThrow(); }); // Test for proper escaping of table and column names in foreign key definitions diff --git a/modules/sqlite/sqlite-utils/table-query-gen.ts b/modules/sqlite/sqlite-utils/table-query-gen.ts index 7803821..fbf29ba 100644 --- a/modules/sqlite/sqlite-utils/table-query-gen.ts +++ b/modules/sqlite/sqlite-utils/table-query-gen.ts @@ -5,6 +5,10 @@ export function assembleCreateTableQuery( columns: string[], tableLevelConstraints: string[] ): string { + if (columns.length === 0) + throw new Error( + `No columns for table ${tableName}` + ); const tableDefinition = [...columns, ...tableLevelConstraints] .filter(Boolean) .join(", "); @@ -15,9 +19,24 @@ export function createTableLevelConstraint( fieldName: string, definition: FieldDefinition ): string | null { + console.log({ fieldName, definition }); if (definition.foreignKey) { - const [referencedTable, referencedField] = definition.foreignKey.split("("); - return `FOREIGN KEY (\`${fieldName}\`) REFERENCES ${referencedTable}(${referencedField}`; + const [referencedTable, referencedField]: string[] = + definition.foreignKey.split("("); + console.log({ + referencedField, + referencedTable, + }); + + if (!referencedField) + throw new Error( + `No referenced field for foreign key ${definition.foreignKey}` + ); + if (!referencedTable) + throw new Error( + `No referenced table for foreign key ${definition.foreignKey}` + ); + return `FOREIGN KEY (\`${fieldName}\`) REFERENCES ${referencedTable.trim()}(${referencedField}`.trim(); } return null; } From 520ca76d2d6808cfc582adea682c6c5b5463fca6 Mon Sep 17 00:00:00 2001 From: brandon-schabel Date: Fri, 3 Nov 2023 15:54:15 -0700 Subject: [PATCH 3/5] remove console --- modules/sqlite/sqlite-utils/table-query-gen.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/modules/sqlite/sqlite-utils/table-query-gen.ts b/modules/sqlite/sqlite-utils/table-query-gen.ts index fbf29ba..628fdb3 100644 --- a/modules/sqlite/sqlite-utils/table-query-gen.ts +++ b/modules/sqlite/sqlite-utils/table-query-gen.ts @@ -23,10 +23,6 @@ export function createTableLevelConstraint( if (definition.foreignKey) { const [referencedTable, referencedField]: string[] = definition.foreignKey.split("("); - console.log({ - referencedField, - referencedTable, - }); if (!referencedField) throw new Error( From 389f7fd01d6afd913a491a262e7914072722eb8e Mon Sep 17 00:00:00 2001 From: brandon-schabel Date: Sat, 4 Nov 2023 19:00:35 -0700 Subject: [PATCH 4/5] add readItemById --- modules/server/server-example.ts | 2 - modules/sqlite/sqlite-factory.test.ts | 10 +- modules/sqlite/sqlite-factory.ts | 68 ++++++------- modules/sqlite/sqlite-table-factory.test.ts | 8 +- modules/sqlite/sqlite-table-factory.ts | 58 +++++------ .../sqlite/sqlite-utils/crud-fn-utils.test.ts | 95 ++++++++++++++++++- modules/sqlite/sqlite-utils/crud-fn-utils.ts | 72 ++++++++++++++ .../sqlite-utils/crud-string-utils.test.ts | 2 +- .../sqlite/sqlite-utils/crud-string-utils.ts | 67 ++++++++++--- .../sqlite-utils/table-query-gen.test.ts | 4 +- .../sqlite/sqlite-utils/table-query-gen.ts | 3 +- .../create-react-server-routes.tsx | 6 -- plugins/react-server/fullstack-state.tsx | 6 +- 13 files changed, 284 insertions(+), 117 deletions(-) diff --git a/modules/server/server-example.ts b/modules/server/server-example.ts index 4c5e7a5..06395d1 100644 --- a/modules/server/server-example.ts +++ b/modules/server/server-example.ts @@ -26,13 +26,11 @@ const middleware = { const routes = { "/": { GET: (req, mid) => { - console.log({ mid }); return new Response(`Hello World! ${mid?.time?.timestamp}`); }, }, "/random": { GET: (req, mid) => { - console.log({ mid }); return new Response(`Hello World! ${mid?.time?.timestamp}`); }, }, diff --git a/modules/sqlite/sqlite-factory.test.ts b/modules/sqlite/sqlite-factory.test.ts index 35d0be0..b35887d 100644 --- a/modules/sqlite/sqlite-factory.test.ts +++ b/modules/sqlite/sqlite-factory.test.ts @@ -33,7 +33,7 @@ describe("createSqliteFactory", () => { text: "some text", }); - const notes = notesTable.read(); + const notes = notesTable.readAll(); expect(notes).toEqual([{ id: "1", text: "some text" }]); }); @@ -51,7 +51,7 @@ describe("createSqliteFactory", () => { text: "some text", }); - const notes = notesTable.read(); + const notes = notesTable.readAll(); expect(notes).toEqual([{ id: "1", text: "some text" }]); @@ -59,7 +59,7 @@ describe("createSqliteFactory", () => { text: "some text updated", }); - const updatedNotes = notesTable.read(); + const updatedNotes = notesTable.readAll(); expect(updatedNotes).toEqual([{ id: "1", text: "some text updated" }]); }); @@ -77,13 +77,13 @@ describe("createSqliteFactory", () => { text: "some text", }); - const notes = notesTable.read(); + const notes = notesTable.readAll(); expect(notes).toEqual([{ id: "1", text: "some text" }]); notesTable.deleteById("1"); - const updatedNotes = notesTable.read(); + const updatedNotes = notesTable.readAll(); expect(updatedNotes).toEqual([]); }); diff --git a/modules/sqlite/sqlite-factory.ts b/modules/sqlite/sqlite-factory.ts index 7a09048..2fbf371 100644 --- a/modules/sqlite/sqlite-factory.ts +++ b/modules/sqlite/sqlite-factory.ts @@ -5,16 +5,6 @@ import { sqliteTableFactory, } from "./sqlite-table-factory"; -export type CreateSqliteFactory = { - create: (item: SQLiteSchemaInfer) => Promise; - read: () => Promise[]>; - update: ( - id: number, - item: Partial> - ) => Promise; - deleteById: (id: number) => Promise; -}; - type CreateSqliteFactoryParams = { db: Database; debug?: boolean; @@ -25,7 +15,7 @@ type DBTableFactoryParams = Omit< SqliteTableFactoryParams, "db" > & { - debug: boolean; + debug?: boolean; }; // Mapping of SQLite types to TypeScript types. @@ -41,12 +31,12 @@ export type SQLiteToTypeScriptTypes = { export type SQLiteDataTypes = keyof SQLiteToTypeScriptTypes; export type FieldDefinition = { - type: SQLiteDataTypes; // The data type of the field - primaryKey?: boolean; // Whether the field is a primary key - unique?: boolean; // Whether the field should be unique - foreignKey?: string; // Reference to another table (foreign key) - notNull?: boolean; // Whether the field can be null - defaultValue?: string | number; // Default value for the field + type: SQLiteDataTypes; + primaryKey?: boolean; + unique?: boolean; + foreignKey?: string; + required?: boolean; + defaultValue?: string | number; }; // Mapped type that takes a schema with SQLite types and returns a schema with TypeScript types. @@ -58,19 +48,6 @@ export type SQLiteSchemaInfer = { export type SchemaMap = Partial>; -// example -const sqlitePersonTableSchema = { - id: { type: "TEXT" }, - age: { type: "INTEGER" }, - name: { type: "TEXT" }, - createdAt: { type: "DATE" }, -} satisfies SchemaMap; - -type Person = SQLiteSchemaInfer; - -// TODO implement into the sqlite factory -type PersonTableSchema = SQLiteSchemaInfer; - export const createTableSchema = ( schema: Schema ): string => { @@ -83,16 +60,6 @@ export const getType = ( return undefined as any as SQLiteSchemaInfer; }; - - -// This should now have the correct types. -let person: PersonTableSchema = { - id: "some-id", - age: 42, - name: "some-name", - createdAt: new Date(), -}; - export function createSqliteFactory({ db, debug = false, @@ -126,3 +93,24 @@ export function createSqliteFactory({ return { dbTableFactory }; } + +// This should now have the correct types. +// let person: PersonTableSchema = { +// id: "some-id", +// age: 42, +// name: "some-name", +// createdAt: new Date(), +// }; + +// // example +// const sqlitePersonTableSchema = { +// id: { type: "TEXT" }, +// age: { type: "INTEGER" }, +// name: { type: "TEXT" }, +// createdAt: { type: "DATE" }, +// } satisfies SchemaMap; + +// type Person = SQLiteSchemaInfer; + +// // TODO implement into the sqlite factory +// type PersonTableSchema = SQLiteSchemaInfer; diff --git a/modules/sqlite/sqlite-table-factory.test.ts b/modules/sqlite/sqlite-table-factory.test.ts index 7962ca3..b4125f5 100644 --- a/modules/sqlite/sqlite-table-factory.test.ts +++ b/modules/sqlite/sqlite-table-factory.test.ts @@ -32,7 +32,7 @@ describe("sqliteTableFactory", () => { test("should insert an item into the database using the factory", () => { const item = { id: "1", name: "John", age: 30 }; factory.create(item); - const items = factory.read(); + const items = factory.readAll(); expect(items.length).toBe(1); expect(items[0]).toEqual(item); }); @@ -42,7 +42,7 @@ describe("sqliteTableFactory", () => { mockDb .query("INSERT INTO test (id, name, age) VALUES (?, ?, ?)") .run(item.id, item.name, item.age); - const items = factory.read(); + const items = factory.readAll(); expect(items.length).toBe(1); expect(items[0]).toEqual(item); }); @@ -54,7 +54,7 @@ describe("sqliteTableFactory", () => { .run(item.id, item.name, item.age); const updatedName = "John Doe"; factory.update(item.id, { name: updatedName }); - const items = factory.read(); + const items = factory.readAll(); expect(items.length).toBe(1); expect(items[0]).toEqual({ ...item, name: updatedName }); }); @@ -64,7 +64,7 @@ describe("sqliteTableFactory", () => { .query("INSERT INTO test (id, name, age) VALUES (?, ?, ?)") .run(item.id, item.name, item.age); factory.deleteById(item.id); - const items = factory.read(); + const items = factory.readAll(); expect(items.length).toBe(0); }); }); diff --git a/modules/sqlite/sqlite-table-factory.ts b/modules/sqlite/sqlite-table-factory.ts index 3a268e4..2c71840 100644 --- a/modules/sqlite/sqlite-table-factory.ts +++ b/modules/sqlite/sqlite-table-factory.ts @@ -1,13 +1,11 @@ import Database from "bun:sqlite"; -import { - CreateSqliteFactory, - SQLiteSchemaInfer, - SchemaMap, -} from "./sqlite-factory"; +import { SQLiteSchemaInfer, SchemaMap } from "./sqlite-factory"; import { createItem, deleteItemById, + readItemById, readItems, + readItemsWhere, updateItem, } from "./sqlite-utils/crud-fn-utils"; import { createTableQuery } from "./sqlite-utils/table-query-gen"; @@ -51,35 +49,27 @@ export function sqliteTableFactory< db.query(createTableQuery({ tableName, schema, debug })).run(); - // Pass necessary context to external CRUD functions - function create(item: TranslatedSchema) { - return createItem(db, tableName, log, item); - } - - function read(): TranslatedSchema[] { - return readItems(db, tableName, log) as TranslatedSchema[]; - } - - function update( - id: string | number, - item: Partial> - ) { - updateItem(db, tableName, log, id, item); - } - - function deleteById(id: number | string) { - deleteItemById(db, tableName, log, id); - } - - function infer(): CreateSqliteFactory { - return undefined as any as CreateSqliteFactory; - } - return { - create, - read, - update, - deleteById, - infer, + readItemsWhere(where: Partial) { + return readItemsWhere(db, tableName, log, where); + }, + create(item: TranslatedSchema) { + return createItem(db, tableName, log, item); + }, + readAll(): TranslatedSchema[] { + return readItems(db, tableName, log) as TranslatedSchema[]; + }, + readById(id: string | number) { + return readItemById(db, tableName, log, id); + }, + update(id: string | number, item: Partial>) { + return updateItem(db, tableName, log, id, item); + }, + deleteById(id: number | string) { + return deleteItemById(db, tableName, log, id); + }, + infer(): TranslatedSchema { + return undefined as any as TranslatedSchema; + }, }; } diff --git a/modules/sqlite/sqlite-utils/crud-fn-utils.test.ts b/modules/sqlite/sqlite-utils/crud-fn-utils.test.ts index 1a4c69d..0a971c6 100644 --- a/modules/sqlite/sqlite-utils/crud-fn-utils.test.ts +++ b/modules/sqlite/sqlite-utils/crud-fn-utils.test.ts @@ -1,8 +1,9 @@ import Database from "bun:sqlite"; -import { beforeEach, describe, expect, test } from "bun:test"; +import { beforeEach, describe, expect, it, test } from "bun:test"; import { SchemaMap } from "../sqlite-factory"; import { createItem, + createWhereClause, deleteItemById, readItems, updateItem, @@ -60,3 +61,95 @@ describe("Database utility functions", () => { expect(items).toEqual([]); }); }); + +describe("createWhereClause", () => { + it("should create a WHERE clause and parameters for a SQL query", () => { + const where = { id: 1, name: "John" }; + const result = createWhereClause(where); + + expect(result).toEqual({ + whereClause: "id = ? AND name = ?", + parameters: [1, "John"], + }); + }); + + it("should handle an empty object", () => { + const where = {}; + const result = createWhereClause(where); + + expect(result).toEqual({ + whereClause: "", + parameters: [], + }); + }); + + it("should handle null and undefined values", () => { + const where = { id: null, name: undefined }; + const result = createWhereClause(where); + + expect(result).toEqual({ + whereClause: "id = ? AND name = ?", + parameters: [null, undefined], + }); + }); + + it('should handle more than two properties', () => { + const where = { id: 1, name: 'John', age: 30 }; + const result = createWhereClause(where); + + expect(result).toEqual({ + whereClause: 'id = ? AND name = ? AND age = ?', + parameters: [1, 'John', 30], + }); + }); + + it('should handle boolean values', () => { + const where = { isActive: true }; + const result = createWhereClause(where); + + expect(result).toEqual({ + whereClause: 'isActive = ?', + parameters: [true], + }); + }); + + it('should handle numeric string values', () => { + const where = { id: '1' }; + const result = createWhereClause(where); + + expect(result).toEqual({ + whereClause: 'id = ?', + parameters: ['1'], + }); + }); + + it('should handle more than two properties', () => { + const where = { id: 1, name: 'John', age: 30 }; + const result = createWhereClause(where); + + expect(result).toEqual({ + whereClause: 'id = ? AND name = ? AND age = ?', + parameters: [1, 'John', 30], + }); + }); + + it('should handle boolean values', () => { + const where = { isActive: true }; + const result = createWhereClause(where); + + expect(result).toEqual({ + whereClause: 'isActive = ?', + parameters: [true], + }); + }); + + it('should handle numeric string values', () => { + const where = { id: '1' }; + const result = createWhereClause(where); + + expect(result).toEqual({ + whereClause: 'id = ?', + parameters: ['1'], + }); + }); +}); diff --git a/modules/sqlite/sqlite-utils/crud-fn-utils.ts b/modules/sqlite/sqlite-utils/crud-fn-utils.ts index f9989a5..1bce657 100644 --- a/modules/sqlite/sqlite-utils/crud-fn-utils.ts +++ b/modules/sqlite/sqlite-utils/crud-fn-utils.ts @@ -23,6 +23,78 @@ export function createItem< return []; } +// Modify the readItems function to include an optional id parameter. +export function readItemById< + Schema extends SchemaMap, + TranslatedSchema extends SQLiteSchemaInfer = SQLiteSchemaInfer +>( + db: Database, + tableName: string, + log: (msg: any) => void, + id: string | number // Add an optional id parameter +): TranslatedSchema { + const query = selectItemByIdQueryString(tableName, id); + log(query); + // Use the ID in the parameterized query to prevent SQL injection. + const data = db.query(query).get({ $id: id }) as TranslatedSchema; + return data; +} +// Assuming SchemaMap and SQLiteSchemaInfer are defined types, as implied by the original function signature + +// This type represents the shape of the 'where' parameter +type Where = Partial; + +// This interface will be used to type the return value of createWhereClause +interface WhereClauseResult { + whereClause: string; + parameters: { [key: string]: any }; +} + +// Function to create a WHERE clause and parameters for a SQL query +export function createWhereClause>( + where: Where +): WhereClauseResult { + const keys = Object.keys(where) as Array; + const whereClause = keys.map((key) => `${String(key)} = ?`).join(" AND "); + const parameters = keys.map((key) => where[key]); + + return { whereClause, parameters }; +} + +export function readItemsWhere< + Schema extends SchemaMap, + TranslatedSchema extends SQLiteSchemaInfer = SQLiteSchemaInfer +>( + db: Database, + tableName: string, + log: (msg: any) => void, + where: Where +): TranslatedSchema[] { + const { whereClause, parameters } = + createWhereClause(where); + + // The query string now uses '?' placeholders for parameters + const queryString = `SELECT * FROM ${tableName} WHERE ${whereClause};`; + log(queryString); // Log the query string for debugging purposes + + // Prepare the statement with the queryString + const statement = db.prepare(queryString); + + // Assuming the .all() method on the prepared statement executes the query + // and retrieves all the results after binding the parameters + const data = statement.all(parameters) as TranslatedSchema[]; + + return data; // Return the query results +} + +// In your crud-string-utils file, add a function to create a SQL query string to select by ID. +export function selectItemByIdQueryString( + tableName: string, + id: string | number +): string { + return `SELECT * FROM ${tableName} WHERE id = $id;`; +} + export function readItems< Schema extends SchemaMap, TranslatedSchema extends SQLiteSchemaInfer = SQLiteSchemaInfer diff --git a/modules/sqlite/sqlite-utils/crud-string-utils.test.ts b/modules/sqlite/sqlite-utils/crud-string-utils.test.ts index e2e0e79..0786539 100644 --- a/modules/sqlite/sqlite-utils/crud-string-utils.test.ts +++ b/modules/sqlite/sqlite-utils/crud-string-utils.test.ts @@ -11,7 +11,7 @@ test("insertQueryString", () => { const tableName = "users"; const item = { name: "Alice", age: 30 }; const query = insertQueryString(tableName, item); - const expectedQuery = `INSERT INTO users VALUES (?, ?)`; + const expectedQuery = `INSERT INTO users (name, age) VALUES (?, ?)`; expect(query).toBe(expectedQuery); }); diff --git a/modules/sqlite/sqlite-utils/crud-string-utils.ts b/modules/sqlite/sqlite-utils/crud-string-utils.ts index cb87d31..c0ec4a9 100644 --- a/modules/sqlite/sqlite-utils/crud-string-utils.ts +++ b/modules/sqlite/sqlite-utils/crud-string-utils.ts @@ -1,23 +1,60 @@ -export function insertQueryString( +export function insertQueryString>( tableName: string, item: Item -) { - const valuesArray = Object.values(item); - const placeholders = valuesArray.map(() => "?").join(", "); - return `INSERT INTO ${tableName} VALUES (${placeholders})`; +): string { + // Define a whitelist for table names if they are dynamic or ensure tableName is sanitized. + const safeTableName = escapeIdentifier(tableName); + + // Get the column names and placeholders. + const columns = Object.keys(item) + .map((column) => escapeIdentifier(column)) + .join(", "); + const placeholders = Object.values(item) + .map(() => "?") + .join(", "); + + // Handle the case where the item might be empty. + if (columns.length === 0 || placeholders.length === 0) { + throw new Error("No data provided for insert."); + } + + return `INSERT INTO ${safeTableName} (${columns}) VALUES (${placeholders})`; } -export function selectAllTableQueryString(tableName: string) { - return `SELECT * FROM ${tableName};`; +function escapeIdentifier(identifier: string): string { + // This is a simplistic approach and might not cover all edge cases. + if (!identifier.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) { + throw new Error("Invalid identifier"); + } + return identifier; // Assuming the identifier is safe, otherwise escape it properly. +} +export function selectAllTableQueryString(tableName: string): string { + // Validate or escape the tableName to prevent SQL injection + const safeTableName = escapeIdentifier(tableName); + return `SELECT * FROM ${safeTableName};`; } -export function deleteQueryString(tableName: string) { - return `DELETE FROM ${tableName} WHERE id = $id;`; +export function deleteQueryString(tableName: string): string { + // Validate or escape the tableName to prevent SQL injection + const safeTableName = escapeIdentifier(tableName); + return `DELETE FROM ${safeTableName} WHERE id = $id;`; } -export function updateQueryString(tableName: string, item: any) { - const updateFields = Object.keys(item) - .map((key) => `${key} = $${key}`) - .join(", "); - return `UPDATE ${tableName} SET ${updateFields} WHERE id = $id;`; - } \ No newline at end of file +export function updateQueryString( + tableName: string, + item: Record +): string { + // Validate or escape the tableName to prevent SQL injection + const safeTableName = escapeIdentifier(tableName); + + const updateFields = Object.keys(item) + .map((key) => `${escapeIdentifier(key)} = $${key}`) + .join(", "); + + // Check if the updateFields string is empty and throw an error if it is + if (updateFields.length === 0) { + throw new Error("No fields to update were provided."); + } + + return `UPDATE ${safeTableName} SET ${updateFields} WHERE id = $id;`; +} diff --git a/modules/sqlite/sqlite-utils/table-query-gen.test.ts b/modules/sqlite/sqlite-utils/table-query-gen.test.ts index b5a1587..9d1c55e 100644 --- a/modules/sqlite/sqlite-utils/table-query-gen.test.ts +++ b/modules/sqlite/sqlite-utils/table-query-gen.test.ts @@ -68,7 +68,7 @@ describe("createColumnDefinition", () => { }); it("should generate a column definition with a NOT NULL constraint", () => { - const definition: FieldDefinition = { type: "INTEGER", notNull: true }; + const definition: FieldDefinition = { type: "INTEGER", required: true }; const result = createColumnDefinition("age", definition); expect(result).toBe("`age` INTEGER NOT NULL"); }); @@ -91,7 +91,7 @@ describe("createColumnDefinition", () => { it("should generate a column definition with multiple constraints", () => { const definition: FieldDefinition = { type: "INTEGER", - notNull: true, + required: true, unique: true, defaultValue: 0, }; diff --git a/modules/sqlite/sqlite-utils/table-query-gen.ts b/modules/sqlite/sqlite-utils/table-query-gen.ts index 628fdb3..6e8e96b 100644 --- a/modules/sqlite/sqlite-utils/table-query-gen.ts +++ b/modules/sqlite/sqlite-utils/table-query-gen.ts @@ -19,7 +19,6 @@ export function createTableLevelConstraint( fieldName: string, definition: FieldDefinition ): string | null { - console.log({ fieldName, definition }); if (definition.foreignKey) { const [referencedTable, referencedField]: string[] = definition.foreignKey.split("("); @@ -47,7 +46,7 @@ export function createColumnDefinition( if (definition.primaryKey) constraints.push("PRIMARY KEY"); if (definition.unique) constraints.push("UNIQUE"); - if (definition.notNull) constraints.push("NOT NULL"); + if (definition.required) constraints.push("NOT NULL"); if (definition.defaultValue !== undefined) { constraints.push(`DEFAULT ${definition.defaultValue}`); } diff --git a/plugins/react-server/create-react-server-routes.tsx b/plugins/react-server/create-react-server-routes.tsx index a7bd0a0..41c1d3c 100644 --- a/plugins/react-server/create-react-server-routes.tsx +++ b/plugins/react-server/create-react-server-routes.tsx @@ -20,12 +20,6 @@ export const createReactServerRoutes = async < // change ./ to just / for buildEntry const cleanedBuildEntry = buildEntry.replace("./", "/"); - console.log({ - cleanedBuildEntry, - middlewareConfig, - appState, - buildEntry, - }); const routes: Routes = { "/": { GET: await createReactStreamHandler({ diff --git a/plugins/react-server/fullstack-state.tsx b/plugins/react-server/fullstack-state.tsx index e754a31..d0eef2e 100644 --- a/plugins/react-server/fullstack-state.tsx +++ b/plugins/react-server/fullstack-state.tsx @@ -1,5 +1,5 @@ import React from "react"; -// this file is just an example +// this file is just an example import { createContext, useEffect, useState } from "react"; export type ContextStateT = { @@ -70,7 +70,6 @@ const clientToServerStateKeyUpdate = async < }), }); const json = await res.json(); - console.log({ json }); return json; }; @@ -100,7 +99,6 @@ export const StateProvider = ({ key: Key, value: StateT[Key] ) => { - console.log({ key, value }); if (isServerSide) return null; setState((prev) => ({ @@ -129,8 +127,6 @@ const useClientAppState = () => { const Counter = () => { const { state, updateKey } = useClientAppState(); - console.log({ state, updateKey }); - return (
Count: {state.count}
From 4259ea07f311832f8ccb434047a45080263076e1 Mon Sep 17 00:00:00 2001 From: brandon-schabel Date: Sat, 4 Nov 2023 19:01:20 -0700 Subject: [PATCH 5/5] remove comment --- modules/sqlite/sqlite-utils/crud-fn-utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/sqlite/sqlite-utils/crud-fn-utils.ts b/modules/sqlite/sqlite-utils/crud-fn-utils.ts index 1bce657..321beca 100644 --- a/modules/sqlite/sqlite-utils/crud-fn-utils.ts +++ b/modules/sqlite/sqlite-utils/crud-fn-utils.ts @@ -39,7 +39,6 @@ export function readItemById< const data = db.query(query).get({ $id: id }) as TranslatedSchema; return data; } -// Assuming SchemaMap and SQLiteSchemaInfer are defined types, as implied by the original function signature // This type represents the shape of the 'where' parameter type Where = Partial;