forked from jsnbuchanan/crowd-funder-for-time-pwa
Finalize Dexie-to-SQLite migration prep: docs, circular dep removal, SQL helpers, tests
- Removed all vestigial Dexie/USE_DEXIE_DB references from code and docs - Centralized DB logic in PlatformServiceMixin; resolved logger/databaseUtil circular dependency - Modularized SQL helpers (`$generateInsertStatement`, `$generateUpdateStatement`) and added unit tests - Created/updated migration tracking docs and helper script for cross-machine progress - Confirmed all lint/type checks and tests pass; ready for systematic file migration
This commit is contained in:
@@ -185,3 +185,4 @@ export default class IdentitySection extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -95,3 +95,4 @@ export function useNotifications() {
|
||||
downloadStarted,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export type Account = {
|
||||
publicKeyHex: string;
|
||||
};
|
||||
|
||||
// TODO: When finished with USE_DEXIE_DB, move these fields to Account and move identity and mnemonic here.
|
||||
// TODO: When finished with migration, move these fields to Account and move identity and mnemonic here.
|
||||
export type AccountEncrypted = Account & {
|
||||
identityEncrBase64: string;
|
||||
mnemonicEncrBase64: string;
|
||||
|
||||
@@ -221,3 +221,4 @@ export function createProfileService(
|
||||
): ProfileService {
|
||||
return new ProfileService(axios, partnerApiServer);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ export async function getDexieExportBlob(): Promise<Blob> {
|
||||
* Retrieves all contacts from the Dexie (IndexedDB) database
|
||||
*
|
||||
* This function connects to the Dexie database and retrieves all contact
|
||||
* records. It requires that USE_DEXIE_DB is enabled in the app constants.
|
||||
* records. The migration tools will automatically handle database access through the PlatformServiceMixin.
|
||||
*
|
||||
* The function handles database opening and error conditions, providing
|
||||
* detailed logging for debugging purposes.
|
||||
|
||||
32
src/test/PlatformServiceMixin.test.ts
Normal file
32
src/test/PlatformServiceMixin.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
generateInsertStatement,
|
||||
generateUpdateStatement,
|
||||
} from "@/utils/sqlHelpers";
|
||||
|
||||
describe("sqlHelpers SQL Statement Generation", () => {
|
||||
it("generates correct INSERT statement", () => {
|
||||
const contact = {
|
||||
name: "Alice",
|
||||
age: 30,
|
||||
isActive: true,
|
||||
tags: ["friend"],
|
||||
};
|
||||
const { sql, params } = generateInsertStatement(contact, "contacts");
|
||||
expect(sql).toBe(
|
||||
"INSERT INTO contacts (name, age, isActive, tags) VALUES (?, ?, ?, ?)",
|
||||
);
|
||||
expect(params).toEqual(["Alice", 30, 1, JSON.stringify(["friend"])]);
|
||||
});
|
||||
|
||||
it("generates correct UPDATE statement", () => {
|
||||
const changes = { name: "Bob", isActive: false };
|
||||
const { sql, params } = generateUpdateStatement(
|
||||
changes,
|
||||
"contacts",
|
||||
"id = ?",
|
||||
[42],
|
||||
);
|
||||
expect(sql).toBe("UPDATE contacts SET name = ?, isActive = ? WHERE id = ?");
|
||||
expect(params).toEqual(["Bob", 0, 42]);
|
||||
});
|
||||
});
|
||||
42
src/test/PlatformServiceMixinTest.vue
Normal file
42
src/test/PlatformServiceMixinTest.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>PlatformServiceMixin Test</h2>
|
||||
<button @click="testInsert">Test Insert</button>
|
||||
<button @click="testUpdate">Test Update</button>
|
||||
<pre>{{ result }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Options, Vue } from "vue-facing-decorator";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
|
||||
@Options({
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class PlatformServiceMixinTest extends Vue {
|
||||
result: string = "";
|
||||
|
||||
testInsert() {
|
||||
const contact = {
|
||||
name: "Alice",
|
||||
age: 30,
|
||||
isActive: true,
|
||||
tags: ["friend"],
|
||||
};
|
||||
const { sql, params } = this.$generateInsertStatement(contact, "contacts");
|
||||
this.result = `SQL: ${sql}\nParams: ${JSON.stringify(params)}`;
|
||||
}
|
||||
|
||||
testUpdate() {
|
||||
const changes = { name: "Bob", isActive: false };
|
||||
const { sql, params } = this.$generateUpdateStatement(
|
||||
changes,
|
||||
"contacts",
|
||||
"id = ?",
|
||||
[42],
|
||||
);
|
||||
this.result = `SQL: ${sql}\nParams: ${JSON.stringify(params)}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -48,7 +48,10 @@ import { MASTER_SETTINGS_KEY, type Settings } from "@/db/tables/settings";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database";
|
||||
import { memoryLogs } from "@/db/databaseUtil";
|
||||
import {
|
||||
generateInsertStatement,
|
||||
generateUpdateStatement,
|
||||
} from "@/utils/sqlHelpers";
|
||||
|
||||
// =================================================
|
||||
// TYPESCRIPT INTERFACES
|
||||
@@ -93,6 +96,8 @@ const CACHE_DEFAULTS = {
|
||||
default: 15000, // 15 seconds default TTL
|
||||
} as const;
|
||||
|
||||
const _memoryLogs: string[] = [];
|
||||
|
||||
/**
|
||||
* Enhanced mixin that provides cached platform service access and utility methods
|
||||
* with smart caching layer for ultimate performance optimization
|
||||
@@ -123,7 +128,7 @@ export const PlatformServiceMixin = {
|
||||
* Provides direct access to memoryLogs without requiring databaseUtil import
|
||||
*/
|
||||
$memoryLogs(): string[] {
|
||||
return memoryLogs;
|
||||
return _memoryLogs;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1117,6 +1122,40 @@ export const PlatformServiceMixin = {
|
||||
async $logAndConsole(message: string, isError = false): Promise<void> {
|
||||
return logger.toConsoleAndDb(message, isError);
|
||||
},
|
||||
|
||||
$appendToMemoryLogs(message: string): void {
|
||||
_memoryLogs.push(`${new Date().toISOString()}: ${message}`);
|
||||
if (_memoryLogs.length > 1000) {
|
||||
_memoryLogs.splice(0, _memoryLogs.length - 1000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Public wrapper for generateInsertStatement
|
||||
*/
|
||||
$generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
): { sql: string; params: unknown[] } {
|
||||
return generateInsertStatement(model, tableName);
|
||||
},
|
||||
|
||||
/**
|
||||
* Public wrapper for generateUpdateStatement
|
||||
*/
|
||||
$generateUpdateStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
whereClause: string,
|
||||
whereParams: unknown[] = [],
|
||||
): { sql: string; params: unknown[] } {
|
||||
return generateUpdateStatement(
|
||||
model,
|
||||
tableName,
|
||||
whereClause,
|
||||
whereParams,
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1190,6 +1229,18 @@ export interface IPlatformServiceMixin {
|
||||
|
||||
// New additions
|
||||
$logs(): Promise<Array<Record<string, unknown>>>;
|
||||
|
||||
// New additions
|
||||
$generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
): { sql: string; params: unknown[] };
|
||||
$generateUpdateStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
whereClause: string,
|
||||
whereParams?: unknown[],
|
||||
): { sql: string; params: unknown[] };
|
||||
}
|
||||
|
||||
// TypeScript declaration merging to eliminate (this as any) type assertions
|
||||
@@ -1296,5 +1347,17 @@ declare module "@vue/runtime-core" {
|
||||
|
||||
// New additions
|
||||
$logs(): Promise<Array<Record<string, unknown>>>;
|
||||
|
||||
// New additions
|
||||
$generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
): { sql: string; params: unknown[] };
|
||||
$generateUpdateStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
whereClause: string,
|
||||
whereParams?: unknown[],
|
||||
): { sql: string; params: unknown[] };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,19 @@
|
||||
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
const _memoryLogs: string[] = [];
|
||||
|
||||
export function appendToMemoryLogs(message: string): void {
|
||||
_memoryLogs.push(`${new Date().toISOString()}: ${message}`);
|
||||
if (_memoryLogs.length > 1000) {
|
||||
_memoryLogs.splice(0, _memoryLogs.length - 1000);
|
||||
}
|
||||
}
|
||||
|
||||
export function getMemoryLogs(): string[] {
|
||||
return [..._memoryLogs];
|
||||
}
|
||||
|
||||
export function safeStringify(obj: unknown) {
|
||||
const seen = new WeakSet();
|
||||
|
||||
|
||||
@@ -276,3 +276,4 @@ export const NotificationMixin = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
67
src/utils/sqlHelpers.ts
Normal file
67
src/utils/sqlHelpers.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* SQL Statement Generation Helpers
|
||||
* Provides utility functions for generating parameterized SQL INSERT and UPDATE statements.
|
||||
*
|
||||
* Author: Matthew Raymer
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generates SQL INSERT statement and parameters from a model object
|
||||
* @param model - The object to insert
|
||||
* @param tableName - The table name
|
||||
* @returns { sql, params } - SQL string and parameter array
|
||||
*/
|
||||
export function generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
): { sql: string; params: unknown[] } {
|
||||
const columns = Object.keys(model).filter((key) => model[key] !== undefined);
|
||||
const values = Object.values(model)
|
||||
.filter((value) => value !== undefined)
|
||||
.map((value) => {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
if (typeof value === "boolean") return value ? 1 : 0;
|
||||
return value;
|
||||
});
|
||||
const placeholders = values.map(() => "?").join(", ");
|
||||
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
return { sql: insertSql, params: values };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates SQL UPDATE statement and parameters from a model object
|
||||
* @param model - The object with fields to update
|
||||
* @param tableName - The table name
|
||||
* @param whereClause - The WHERE clause (e.g. "id = ?")
|
||||
* @param whereParams - Parameters for the WHERE clause
|
||||
* @returns { sql, params } - SQL string and parameter array
|
||||
*/
|
||||
export function generateUpdateStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
whereClause: string,
|
||||
whereParams: unknown[] = [],
|
||||
): { sql: string; params: unknown[] } {
|
||||
const setClauses: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
Object.entries(model).forEach(([key, value]) => {
|
||||
setClauses.push(`${key} = ?`);
|
||||
let convertedValue = value ?? null;
|
||||
if (convertedValue !== null) {
|
||||
if (typeof convertedValue === "object") {
|
||||
convertedValue = JSON.stringify(convertedValue);
|
||||
} else if (typeof convertedValue === "boolean") {
|
||||
convertedValue = convertedValue ? 1 : 0;
|
||||
}
|
||||
}
|
||||
params.push(convertedValue);
|
||||
});
|
||||
if (setClauses.length === 0) {
|
||||
throw new Error("No valid fields to update");
|
||||
}
|
||||
const sql = `UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause}`;
|
||||
return { sql, params: [...params, ...whereParams] };
|
||||
}
|
||||
Reference in New Issue
Block a user