diff --git a/dev-server-api/bun.lockb b/dev-server-api/bun.lockb index d694e621..63511a13 100755 Binary files a/dev-server-api/bun.lockb and b/dev-server-api/bun.lockb differ diff --git a/dev-server-api/package.json b/dev-server-api/package.json index f5cb3674..488a40dc 100644 --- a/dev-server-api/package.json +++ b/dev-server-api/package.json @@ -19,6 +19,7 @@ "better-sqlite3": "^11.0.0", "kysely": "^0.27.3", "kysely-bun-sqlite": "^0.3.2", + "level": "^8.0.1", "redaxios": "^0.5.1", "winterspec": "0.0.81", "zod": "^3.22.4" diff --git a/dev-server-api/src/db/level-db.ts b/dev-server-api/src/db/level-db.ts new file mode 100644 index 00000000..7a363141 --- /dev/null +++ b/dev-server-api/src/db/level-db.ts @@ -0,0 +1,98 @@ +import { Level } from "level" +import { z } from "zod" +import { DBSchema, type DBSchemaType } from "./schema" + +// Create a wrapper class for Level with Zod validation +export class ZodLevelDatabase { + private db: Level + + constructor(location: string) { + this.db = new Level(location) + } + + async get( + collection: K, + id: string + ): Promise { + const key = `${collection}:${id}` + const data = await this.db.get(key) + return DBSchema.shape[collection].parse(JSON.parse(data)) as any + } + + async put( + collection: K, + value: DBSchemaType[K] + ): Promise { + const idkey = `${collection}_id` + const valueLoose: any = value + if (!valueLoose[idkey]) { + // generate an id using the "count" key + let count = await this.db.get(`${collection}:count`) + if (!count) count = 1 + ;(value as any)[idkey] = count + await this.db.put(`${collection}:count`, count + 1) + } + const key = `${collection}:${valueLoose[idkey]}` + const validatedData = DBSchema.shape[collection].parse(value) + await this.db.put(key, JSON.stringify(validatedData)) + } + + async del( + collection: K, + id: string + ): Promise { + const key = `${collection}:${id}` + await this.db.del(key) + } + + async find( + collection: K, + partialObject: Partial + ): Promise { + const results: DBSchemaType[K][] = [] + const schema = DBSchema.shape[collection] + + for await (const [key, value] of this.db.iterator({ + gte: `${collection}:`, + lte: `${collection}:\uffff`, + })) { + try { + const parsedValue = schema.parse(JSON.parse(value)) + if (this.matchesPartialObject(parsedValue, partialObject)) { + return parsedValue as any + } + } catch (error) { + console.error(`Error parsing value for key ${key}:`, error) + } + } + + return null + } + + async findOrThrow( + collection: K, + partialObject: Partial + ): Promise { + const result = await this.find(collection, partialObject) + if (!result) { + throw new Error( + `No record in "${collection}" matches query ${JSON.stringify( + partialObject + )}` + ) + } + return result + } + + private matchesPartialObject( + fullObject: T, + partialObject: Partial + ): boolean { + for (const [key, value] of Object.entries(partialObject)) { + if (fullObject[key as keyof T] !== value) { + return false + } + } + return true + } +} diff --git a/dev-server-api/src/db/schema.ts b/dev-server-api/src/db/schema.ts new file mode 100644 index 00000000..f0079d3e --- /dev/null +++ b/dev-server-api/src/db/schema.ts @@ -0,0 +1,64 @@ +import { z } from "zod" + +// Helper function for nullable fields +const nullableText = () => z.string().nullable() + +// PackageInfo schema +export const PackageInfoSchema = z.object({ + package_info_id: z.number().int(), + name: z.string(), +}) + +// DevPackageExample schema +export const DevPackageExampleSchema = z.object({ + dev_package_example_id: z.number().int(), + file_path: z.string(), + export_name: nullableText(), + tscircuit_soup: z.any().nullable(), // Using any for JSON type + completed_edit_events: z.any().nullable(), // Using any for JSON type + error: nullableText(), + is_loading: z.boolean(), + soup_last_updated_at: nullableText(), + edit_events_last_updated_at: nullableText(), + edit_events_last_applied_at: nullableText(), + last_updated_at: nullableText(), +}) + +// ExportRequest schema +export const ExportRequestSchema = z.object({ + export_request_id: z.number().int(), + example_file_path: nullableText(), + export_parameters: z.any().nullable(), // Using any for JSON type + export_name: nullableText(), + is_complete: z.boolean(), + has_error: z.boolean(), + error: nullableText(), + created_at: nullableText(), +}) + +// ExportFile schema +export const ExportFileSchema = z.object({ + export_file_id: z.number().int(), + file_name: nullableText(), + file_content: z.instanceof(Buffer).nullable(), // For BLOB type + is_complete: z.boolean(), + export_request_id: z.number().int().nullable(), + created_at: nullableText(), +}) + +// Combined DBSchema +export const DBSchema = z.object({ + package_info: PackageInfoSchema, + dev_package_example: DevPackageExampleSchema, + export_request: ExportRequestSchema, + export_file: ExportFileSchema, +}) + +// TypeScript type inference +export type DBSchemaType = z.infer + +// You can also export individual types if needed +export type PackageInfo = z.infer +export type DevPackageExample = z.infer +export type ExportRequest = z.infer +export type ExportFile = z.infer