diff --git a/jsr.json b/jsr.json index f030a10..2d89216 100644 --- a/jsr.json +++ b/jsr.json @@ -1,5 +1,5 @@ { "name": "@netron/surreal", - "version": "0.1.25", + "version": "0.1.26", "exports": "./index.ts" } diff --git a/package.json b/package.json index f45d6ca..bbb3867 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@netron/surreal", - "version": "0.1.25", + "version": "0.1.26", "description": "type safe surrealdb", "main": "index.ts", "scripts": { diff --git a/src/NSurreal.ts b/src/NSurreal.ts index 141665b..c9c59ea 100644 --- a/src/NSurreal.ts +++ b/src/NSurreal.ts @@ -1,22 +1,26 @@ -import { ConnectionOptions, Surreal } from "surrealdb.js"; -import { generate_unique_hash_repeatably } from "./hash"; -import jsonToZod from "json-to-zod"; +import { type ConnectionOptions, Surreal } from "surrealdb.js"; import fs from "fs"; import JsonToTS from "./jsontots"; /** TypeSafe SurrealDB Client https://jsr.io/@netron/surreal based on surrealdb.js * See https://surrealdb.com/docs/surrealdb/integration/sdks/javascript */ -export class NSurreal = {}> { +export class NSurreal> { client: Surreal; timer: NodeJS.Timeout; timestamp_connected: Date | undefined; url: string | undefined; opts: ConnectionOptions | undefined; - output_path: string = "./src/generated"; + output_path = "./src/generated"; debug = false; - constructor(options?: { output_path?: string; debug?: boolean }) { + skip_write = false; + + constructor(options?: { + output_path?: string; + debug?: boolean; + skip_write?: boolean; + }) { if (options?.output_path) { this.output_path = options.output_path; } @@ -25,15 +29,19 @@ export class NSurreal = {}> { this.debug = options.debug; } + if (options?.skip_write) { + this.skip_write = options.skip_write; + } + this.client = new Surreal(); this.timer = setInterval(() => { if (!this.timestamp_connected) return; - const diff = new Date().getTime() - this.timestamp_connected!.getTime(); + const diff = new Date().getTime() - this.timestamp_connected.getTime(); if (diff > 1000 * 60) { if (this.client) { - this.client.close(); + this.client.close().catch(console.error); } - this.connect(this.url!, this.opts!); + this.connect(this.url!, this.opts).catch(console.error); } }, 10000); } @@ -42,7 +50,7 @@ export class NSurreal = {}> { * https://surrealdb.com/docs/surrealdb/integration/sdks/javascript#connect */ - log(...input: any[]) { + log(...input: Parameters) { if (this.debug) console.log(`NSurreal -`, input); } @@ -97,7 +105,7 @@ export class NSurreal = {}> { const uid = uniqueID as string; // ensure uniqueId starts with capital. - if (uid[0] != uid[0].toUpperCase()) { + if (!uid.startsWith(uid.at(0)?.toUpperCase() ?? "")) { throw new Error("uniqueID must start with a capital letter."); } @@ -115,7 +123,7 @@ export class NSurreal = {}> { this.log("Query result", result); - if (uniqueID) { + if (uniqueID && !this.skip_write) { const uid = uniqueID as string; let querycount = query.split(";").length; @@ -125,7 +133,7 @@ export class NSurreal = {}> { this.log(`Query count: ${querycount}`); - let responseobj: Record = {}; + const responseobj: Record = {}; result.forEach((i, idx) => { responseobj[`${uid}_query${idx + 1}`] = i; @@ -152,7 +160,7 @@ export class NSurreal = {}> { this.log("Error creating directory", err); }); - let outputtype = ts.map((i, idx) => { + const outputtype = ts.map((i, idx) => { if (idx == 0) { let output = i.split("\n").at(0)?.replace("{", "["); output += "\n"; @@ -160,7 +168,7 @@ export class NSurreal = {}> { output += i .split("\n") .slice(1, -1) - .map((x) => x.split(":")[1].slice(0, -1)) + .map((x) => x.split(":").at(1)?.slice(0, -1)) .join(",\n"); output += "\n"; @@ -177,29 +185,28 @@ export class NSurreal = {}> { await fs.promises .writeFile( `${this.output_path}/querytypes/${uid}.ts`, - `import { RecordId } from "surrealdb.js"; + `import type { UUID, RecordId } from "surrealdb.js"; export ${outputtype.join("\n")}` ) .catch((err) => { this.log("Error writing to file", err); }); - let schemas = await this.read_querytypes(); + const schemas = await this.read_querytypes(); await fs.promises.writeFile( `${this.output_path}/combined.ts`, - `import { z } from "zod";\n -${schemas - .map((i) => { - return `import { ${i.replace(".ts", "")} } from "./querytypes/${i.replace( - ".ts", - "" - )}";`; - }) - .join("\n")} + `${schemas + .map((i) => { + return `import type { ${i.replace( + ".ts", + "" + )} } from "./querytypes/${i.replace(".ts", "")}";`; + }) + .join("\n")} export type Queries = {\n${schemas - .map((i) => ` "${i.replace(".ts", "")}": ${i.replace(".ts", "")}`) + .map((i) => ` ${i.replace(".ts", "")}: ${i.replace(".ts", "")};`) .join("\n")}\n}` ); } diff --git a/src/generateschema.ts b/src/generateschema.ts deleted file mode 100644 index d929413..0000000 --- a/src/generateschema.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { NSurreal } from "./NSurreal"; -import { - SR_derive_fields_from_table, - SR_getInfoForDB, -} from "./surreal_helpers"; -import jsonToZod from "json-to-zod"; -import fs from "fs"; - -export async function schema_generate(inputs: { - db: NSurreal; - /** example: src/dbschema.ts */ - fileout?: string; -}) { - const db = inputs.db; - const dbinfo = await SR_getInfoForDB({ db }); - let schema_data = `import { z } from "zod";\n`; - const names: { table: string; zodname: string; typename: string }[] = []; - - for (const table of Object.keys(dbinfo.tables)) { - const rows = await SR_derive_fields_from_table({ db, table }); - const zodname = `TB${table}`; - const typename = `TB${table}_type`; - names.push({ table, zodname, typename }); - const o = jsonToZod(rows, zodname); - schema_data += `export ${o}\nexport type ${typename} = z.infer[number];\n\n`; - } - - let combined = `export type DB = {\n`; - names.forEach((i) => { - combined += ` "${i.table}": ${i.typename}\n`; - }); - - combined += `}\n`; - schema_data += combined; - - await fs.promises.writeFile(inputs.fileout ?? "src/dbschema.ts", schema_data); - - return { schema_data }; -} diff --git a/src/hash.ts b/src/hash.ts deleted file mode 100644 index 5261427..0000000 --- a/src/hash.ts +++ /dev/null @@ -1,16 +0,0 @@ -export async function generate_unique_hash_repeatably(message: string) { - // encode as UTF-8 - const msgBuffer = new TextEncoder().encode(message); - - // hash the message - const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); - - // convert ArrayBuffer to Array - const hashArray = Array.from(new Uint8Array(hashBuffer)); - - // convert bytes to hex string - const hashHex = hashArray - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); - return hashHex; -} diff --git a/src/jsontots/get-names.ts b/src/jsontots/get-names.ts index 899d977..f3e3d88 100644 --- a/src/jsontots/get-names.ts +++ b/src/jsontots/get-names.ts @@ -1,11 +1,11 @@ // import * as pluralize from "pluralize"; import { - TypeStructure, - NameEntry, - NameStructure, + type TypeStructure, + type NameEntry, + type NameStructure, TypeGroup, - TypeDescription, + type TypeDescription, } from "./model"; import { getTypeDescriptionGroup, @@ -71,7 +71,7 @@ function getName( export function getNames( typeStructure: TypeStructure, - rootName: string = "RootObject" + rootName = "RootObject" ): NameEntry[] { return getName(typeStructure, rootName, [], false).names.reverse(); } @@ -83,7 +83,7 @@ function getNameById( types: TypeDescription[], nameMap: NameEntry[] ): string { - let nameEntry = nameMap.find((_) => _.id === id); + const nameEntry = nameMap.find((_) => _.id === id); if (nameEntry) { return nameEntry.name; @@ -109,7 +109,6 @@ function getNameById( .map((key) => parseKeyMetaData(key).keyValue) // .map((name) => (isInsideArray ? pluralize.singular(name) : name)) .map(pascalCase) - // @ts-ignore .map(normalizeInvalidTypeName) .map(pascalCase) // needed because removed symbols might leave first character uncapitalized .map((name) => @@ -122,7 +121,7 @@ function getNameById( break; } - nameMap.push({ id, name: name as string }); + nameMap.push({ id, name: String(name) }); if (!name) { throw new Error("could not getNameById"); @@ -147,11 +146,11 @@ function capitalize(name: string) { return name.charAt(0).toUpperCase() + name.slice(1); } -function normalizeInvalidTypeName(name: string): string { - if (/^[a-zA-Z][a-zA-Z0-9]*$/.test(name)) { - return name; +function normalizeInvalidTypeName(name: string | null): string { + if (/^[a-zA-Z][a-zA-Z0-9]*$/.test(name ?? "null")) { + return String(name); } else { - const noSymbolsName = name.replace(/[^a-zA-Z0-9]/g, ""); + const noSymbolsName = String(name ?? "null").replace(/[^a-zA-Z0-9]/g, ""); const startsWithWordCharacter = /^[a-zA-Z]/.test(noSymbolsName); return startsWithWordCharacter ? noSymbolsName : `_${noSymbolsName}`; } @@ -176,7 +175,7 @@ function getArrayName( return "any"; } else if (typeDesc.arrayOfTypes!.length === 1) { const [idOrPrimitive] = typeDesc.arrayOfTypes!; - return convertToReadableType(idOrPrimitive, types, nameMap); + return convertToReadableType(idOrPrimitive!, types, nameMap); } else { return unionToString(typeDesc, types, nameMap); } diff --git a/src/jsontots/get-type-structure.ts b/src/jsontots/get-type-structure.ts index 18a4e4e..78a86a8 100644 --- a/src/jsontots/get-type-structure.ts +++ b/src/jsontots/get-type-structure.ts @@ -1,6 +1,5 @@ import * as hash from "hash.js"; - -import { TypeDescription, TypeStructure } from "./model"; +import { type TypeDescription, type TypeStructure } from "./model"; import { isHash, getTypeDescriptionGroup, @@ -12,9 +11,10 @@ import { isRecordId, } from "./util"; import { TypeGroup } from "./model"; +import { UUID } from "surrealdb.js"; function createTypeDescription( - typeObj: any | string[], + typeObj: string[], isUnion: boolean ): TypeDescription { if (isArray(typeObj)) { @@ -32,9 +32,9 @@ function createTypeDescription( } function getIdByType( - typeObj: any | string[], + typeObj: Record, types: TypeDescription[], - isUnion: boolean = false + isUnion = false ): string { let typeDesc = types.find((el) => { return typeObjectMatchesTypeDesc(typeObj, el, isUnion); @@ -49,11 +49,11 @@ function getIdByType( } function Hash(content: string): string { - return (hash as any).sha1().update(content).digest("hex"); + return hash.sha1().update(content).digest("hex"); } function typeObjectMatchesTypeDesc( - typeObj: any | string[], + typeObj: string[], typeDesc: TypeDescription, isUnion: boolean ): boolean { @@ -67,16 +67,16 @@ function typeObjectMatchesTypeDesc( } } -function arraysContainSameElements( - arr1: any[], - arr2: string[] | undefined -): boolean { +function arraysContainSameElements(arr1: unknown[], arr2?: unknown[]): boolean { if (arr1 === undefined || arr2 === undefined) return false; return arr1.sort().join("") === arr2.sort().join(""); } -function objectsHaveSameEntries(obj1: any, obj2: any): boolean { +function objectsHaveSameEntries( + obj1: Record | string[] | undefined, + obj2: Record | string[] | undefined +): boolean { if (obj1 === undefined || obj2 === undefined) return false; const entries1 = Object.entries(obj1); @@ -85,13 +85,13 @@ function objectsHaveSameEntries(obj1: any, obj2: any): boolean { const sameLength = entries1.length === entries2.length; const sameTypes = entries1.every(([key, value]) => { - return obj2[key] === value; + return value === obj2![key]; }); return sameLength && sameTypes; } -function getSimpleTypeName(value: any): string { +function getSimpleTypeName(value: unknown): string { if (value === null) { return "null"; } else if (value instanceof Date) { @@ -101,13 +101,15 @@ function getSimpleTypeName(value: any): string { } } -function getTypeGroup(value: any): TypeGroup { +function getTypeGroup(value: unknown): TypeGroup { if (isDate(value)) { return TypeGroup.Date; } else if (isArray(value)) { return TypeGroup.Array; } else if (isRecordId(value)) { return TypeGroup.RecordId; + } else if (value instanceof UUID) { + return TypeGroup.UUID; } else if (isObject(value)) { return TypeGroup.Object; } else { @@ -115,7 +117,10 @@ function getTypeGroup(value: any): TypeGroup { } } -function createTypeObject(obj: any, types: TypeDescription[]): any { +function createTypeObject( + obj: Record, + types: TypeDescription[] +): Record { return Object.entries(obj).reduce((typeObj, [key, value]) => { const { rootTypeId } = getTypeStructure(value, types); @@ -140,7 +145,7 @@ function getMergedObjects( const commonKeys = typeObjects.reduce((commonKeys: string[], typeObj) => { const keys = Object.keys(typeObj!); return commonKeys.filter((key) => keys.includes(key)); - }, allKeys) as string[]; + }, allKeys); const getKeyType = (key: string) => { const typesOfKey = typeObjects @@ -153,7 +158,7 @@ function getMergedObjects( if (typesOfKey.length === 1) { return typesOfKey.pop(); } else { - return getInnerArrayType(typesOfKey, types); + return getInnerArrayType(typesOfKey as string[], types); } }; @@ -185,7 +190,7 @@ function getMergedArrays( .filter(onlyUnique); if (idsOfArrayTypes.length === 1) { - return getIdByType([idsOfArrayTypes.pop()], types); + return getIdByType([idsOfArrayTypes.pop()!], types); } else { return getIdByType([getInnerArrayType(idsOfArrayTypes, types)], types); } @@ -213,7 +218,7 @@ function getMergedUnion( function getInnerArrayType( typesOfArray: string[], types: TypeDescription[] -): string | undefined { +): string { // return inner array type const containsUndefined = typesOfArray.includes("undefined"); @@ -253,7 +258,7 @@ function getInnerArrayType( if (typesOfArray.length === 1) { // one type in array -> that will be our inner type - return typesOfArray.pop() as string; + return typesOfArray.pop()!; } if (typesOfArray.length > 1) { @@ -285,12 +290,12 @@ function getInnerArrayType( } export function getTypeStructure( - targetObj: any, // object that we want to create types for + targetObj: unknown, // object that we want to create types for types: TypeDescription[] = [] ): TypeStructure { switch (getTypeGroup(targetObj)) { case TypeGroup.Array: - const typesOfArray = (targetObj) + const typesOfArray = (targetObj as unknown[]) .map((_) => getTypeStructure(_, types).rootTypeId) .filter(onlyUnique); const arrayInnerTypeId = getInnerArrayType(typesOfArray, types); // create "union type of array types" @@ -302,7 +307,10 @@ export function getTypeStructure( }; case TypeGroup.Object: - const typeObj = createTypeObject(targetObj, types); + const typeObj = createTypeObject( + targetObj as Record, + types + ); const objType = getIdByType(typeObj, types); return { @@ -316,6 +324,12 @@ export function getTypeStructure( types, }; + case TypeGroup.UUID: + return { + rootTypeId: "UUID", + types, + }; + case TypeGroup.Primitive: return { rootTypeId: getSimpleTypeName(targetObj), diff --git a/src/jsontots/index.ts b/src/jsontots/index.ts index 6fb75a0..5b0dcc2 100644 --- a/src/jsontots/index.ts +++ b/src/jsontots/index.ts @@ -1,13 +1,15 @@ import { getTypeStructure, optimizeTypeStructure } from "./get-type-structure"; -import { Options } from "./model"; +import { type Options } from "./model"; import { getInterfaceDescriptions, getInterfaceStringFromDescription, } from "./get-interfaces"; import { getNames } from "./get-names"; -import { isArray, isObject } from "./util"; -export default function JsonToTS(json: any, userOptions?: Options): string[] { +export default function JsonToTS( + json: unknown, + userOptions?: Options +): string[] { const defaultOptions: Options = { rootName: "RootObject", }; diff --git a/src/jsontots/model.ts b/src/jsontots/model.ts index 7b0760f..f6fd2e8 100644 --- a/src/jsontots/model.ts +++ b/src/jsontots/model.ts @@ -4,12 +4,13 @@ export enum TypeGroup { Object, Date, RecordId, + UUID, } export interface TypeDescription { id: string; isUnion?: boolean; - typeObj?: { [index: string]: string }; + typeObj?: Record; arrayOfTypes?: string[]; } diff --git a/src/surreal_helpers.ts b/src/surreal_helpers.ts deleted file mode 100644 index b1bf27b..0000000 --- a/src/surreal_helpers.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { type Surreal } from "surrealdb.js"; -import { z } from "zod"; -import { NSurreal } from "./NSurreal"; - -export async function SR_getInfoForKV({ db }: { db: NSurreal }) { - const result = await db.query("INFO FOR KV;", "info for kv").catch((err) => { - console.log(err); - }); - - // console.log(result[0]); - - const parsed = z - .object({ - namespaces: z.record(z.string(), z.string()), - users: z.record(z.string(), z.string()), - }) - .strict() - .parse(result[0]); - - return parsed; -} - -export async function SR_getInfoForNS({ db }: { db: NSurreal }) { - const result = await db.query("INFO FOR NS;", "info for ns").catch((err) => { - console.log(err); - }); - - const parsed = z - .object({ - databases: z.record(z.string(), z.string()), - tokens: z.record(z.string(), z.string()), - users: z.record(z.string(), z.string()), - }) - .strict() - .parse(result[0]); - - return parsed; -} - -export async function SR_getInfoForDB({ db }: { db: NSurreal }) { - const result = await db.query("INFO FOR DB;", "info_for_db").catch((err) => { - console.log(err); - }); - - if (!result) throw new Error("could not get DB;"); - - // console.log(result[0]?.result); - - const parsed = z - .object({ - analyzers: z.record(z.string(), z.string()), - functions: z.record(z.string(), z.string()), - models: z.record(z.string(), z.string()), - params: z.record(z.string(), z.string()), - scopes: z.record(z.string(), z.string()), - tables: z.record(z.string(), z.string()), - tokens: z.record(z.string(), z.string()), - users: z.record(z.string(), z.string()), - }) - .strict() - .parse(result[0]); - - return parsed; -} - -export async function SR_getInfoForTABLE({ - db, - table, -}: { - db: Surreal; - table: string; -}) { - const result = await db.query(`INFO FOR TABLE ${table};`).catch((err) => { - console.log(err); - }); - - if (!result) throw new Error(`could not get info for tb ${table};`); - - const parsed = z - .object({ - events: z.record(z.string(), z.string()), - fields: z.record(z.string(), z.string()), - indexes: z.record(z.string(), z.string()), - lives: z.record(z.string(), z.string()), - tables: z.record(z.string(), z.string()), - }) - .strict() - .parse(result[0]); - - return parsed; -} - -export async function SR_derive_fields_from_table({ - db, - table, -}: { - db: NSurreal; - table: string; -}) { - const result = await db - .query(`SELECT * FROM ${table} LIMIT 10;`, "derive_fields_from_table") - .catch((err) => { - console.log(err); - }); - - if (!result) throw new Error(`could not get rows from table ${table};`); - - // console.log(result[0]?.result); - - return result[0]!; -} - -export async function SR_info({ db }: { db: NSurreal }) { - const kv = await SR_getInfoForKV({ db }); - const ns = await SR_getInfoForNS({ db }); - const dbinfo = await SR_getInfoForDB({ db }); - - return { kv, ns, db: dbinfo }; -} diff --git a/test.ts b/test.ts index 1b192e7..53b94fc 100644 --- a/test.ts +++ b/test.ts @@ -1,13 +1,8 @@ import { NSurreal } from "./index"; import { z } from "zod"; -import { schema_generate } from "./src/generateschema"; // once generated, you can import the schema like this: import { type Queries } from "./src/generated/combined"; -import Surreal, { RecordId } from "surrealdb.js"; -import { surrealdbWasmEngines } from "surrealdb.wasm/lib/embedded"; -import { SR_info } from "./src/surreal_helpers"; - require("dotenv").config(); async function runtest() { @@ -35,6 +30,11 @@ async function runtest() { // await schema_generate({ db: client, fileout: "dbschema.ts" }); // test should be typed now. + const single = await client.query( + `create test set name = "piet";`, + "CreateQuery" + ); + const res = await client.query( "select *, (select * from vehicle) as vehicles from user; select * from vehicle; info for db;", "ASDF"